Skip to main content

Java Stream 高级用法

· 12 min read
古时的风筝

因为最近做了一个小需求,数据量不大,功能也比较简单,但是计算维度非常多,大部分的计算逻辑其实都可以直接写 SQL 实现,但是那样的话性能就太差了,所以最终采用了在内存中直接计算,这时候 Stream 就有大用处了。

Java Stream 是 JDK 8 开始提供的一种函数式风格的集合操作方法。我之前写过一篇 Java Stream 的文章 - 8000字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理,在掘金社区获得了超过 500 个赞,说明大家还是很喜欢用 Stream 的。

上一篇主要介绍了一些基础用法,这一篇主要就介绍三个功能,排序、分组和 teeing,teeing 是 JDK 12 才出现的。

排序

基本数据类型排序

基本数据类型就是字符串、整型、浮点型这些,也就是要排序的列表中的元素都是这些基本类型的,比如 List<Integer>的。

下面就用一个整型列表举例说明。

正序排序

正序排序,也可以叫做按照自然顺序排序,对于整型来说就是从小到大的。

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
integerList.add(i);
}
List<Integer> collect = integerList.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(collect);

输出结果是 [0, 1, 2, 3, 4],这很简单没什么好说的。

倒序排序

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
integerList.add(i);
}
List<Integer> collect2 = integerList.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(collect2);

倒序排就是从大到小排序,也很简单在 sorted()方法中添加 Comparator.reverseOrder() 就可以了。

sorted() 方法接收的参数是Comparator 函数式接口,在 8000字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理 这篇文章清楚的讲了函数式接口和方法引用,可以翻过去看看。

非基本类型实体排序

基本类型的列表排序很简单,但是在实际项目中用到的情况不太多,经常用到的还是我们自定义类型的排序,比如项目中有一个用户实体、一个订单实体、一个产品实体等。

首先定一个Product实体类:

import lombok.Data;

/**
* @author fengzheng
*/
@Data
public class Product {
/**
* 唯一标示
*/
private Integer id;

/**
* 所属类别
*/
private Integer type;

/**
* 商品名称
*/
private String name;

/**
* 价格
*/
private Double price;

}

按某一个字段排序

对应到我上面定义的这个实体,可以是按照 id 排序,或者按照 price排序。

正序排序

假设按照 price从小到大排序,也就是按照价格由低到高排序。

对应到 SQL 上,可以表示成这样的。

select * from product order by price asc

那用 Stream 实现呢?

List<Product> productList = initProductList();
List<Product> collect = productList.stream()
.sorted(Comparator.comparing(Product::getPrice))
.collect(Collectors.toList());

等价于

List<Product> collect = productList.stream()
.sorted((x,y) -> x.getPrice().compareTo(y.getPrice()))
.collect(Collectors.toList());

等价于

Comparator<Product> comparator = new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return p1.getPrice().compareTo(p2.getPrice());
}
};

List<Product> collect = productList.stream()
.sorted((p1, p2) -> comparator.compare(p1, p2))
.collect(Collectors.toList());

这里面主要由我们提供自定义的就是函数式接口 Comparator,凡是实现了 compare () 方法的都可以。

上面我们自定义的这个 comparator,重载了 compare方法。compare 方法的返回值规则:

  1. 前者小于后者,返回 -1;
  2. 前者大于后者,返回 1;
  3. 前者等于后者,返回 0;

所以可以理解为,如果 compare 返回的是 1, Stream 就会交换两个实体的位置。所以这样一来,倒序排序就很好整了。

倒序排序

可以这样写,使用 reversed() 方法

List<Product> collect = productList.stream()
.sorted(Comparator.comparing(Product::getPrice).reversed())
.collect(Collectors.toList());

或者可以

List<Product> collect = productList.stream()
.sorted(Comparator.comparing(Product::getPrice,Comparator.reverseOrder()))
.collect(Collectors.toList());

还可以直接直接使用compare ,倒序排序就简单了,稍微改一下就好了。

直接用 Lambda 表达式的写法

List<Product> collect = productList.stream()
.sorted((x,y) -> y.getPrice().compareTo(x.getPrice()))
.collect(Collectors.toList());

等价于,抽取出自定义 Comparator的方法

Comparator<Product> comparator = new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return p2.getPrice().compareTo(p1.getPrice());
}
};

List<Product> collect = productList.stream()
.sorted((p1, p2) -> comparator.compare(p1, p2))
.collect(Collectors.toList());

倒序和正序的区别其实就是将 compare()前后两个元素的位置对调一下。

对于大小比较的可以直接用 compare()方法,但是有一些情况可能不止这么简单。没有关系,我们不是可以自定义 Comparator 吗,在 Comparator 重写的 compare 方法中可以加入我们的排序逻辑,不管多么特殊、多么复杂,只要返回一个 int 类型的就可以了。

按照多个字段排序

还有一些情况要按照两个甚至多个字段排序,一个主排序,一个次要排序。比如我们想要先按 type 升序,再按 price 降序。

对应到 SQL 上就像这样

select * from product order by type asc,price desc

那用 Stream 来实现是怎么样的呢?用 thenComparing连接多个要排序的属性。

List<Product> collect = productList.stream().sorted(Comparator.comparing(Product::getType).thenComparing(Product::getPrice, Comparator.reverseOrder())).collect(Collectors.toList());

或者还可以定义两个 Comparator

Comparator<Product> typeComparator = new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return p1.getType().compareTo(p2.getType());
}
};

Comparator<Product> priceComparator = new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return p2.getPrice().compareTo(p1.getPrice());
}
};
List<Product> collect = productList.stream()
.sorted(typeComparator.thenComparing(priceComparator))
.collect(Collectors.toList());

怎么样,一点难度都没有吧。

分组

除了排序,还有一个非常有用而且经常会用的功能就是分组功能。分组功能是 collect()方法提供的功能,返回值是一个字典类型。

根据 type 进行分组

对应到 SQL 中就是下面这样

select * from product group by type

用 Stream 来实现呢,就是下面这样子

Map<Integer, List<Product>> map = productList.stream()
.collect(Collectors.groupingBy(Product::getType));

最后生成的对象是一个 Map 类型,key 是用来作为分组依据的字段值,value 是一个列表,也就是同一组的对象集合。在这个例子中,key 就是 product 对象的 type 属性,value 就是 type 相同的 Product 对象的集合。

如果只是求出每一个组所包含的对象个数,可以这样实现,不用遍历 Map 这么麻烦。

Map<Integer, Long> map = productList.stream()
.collect(Collectors.groupingBy(Product::getType, Collectors.counting()));

根据两个或多个字段分组

有时候我们可能会根据不止一个字段进行分组,比如想按照类别相同且价格相同进行分组。

Map<String, List<Product>> map = productList.stream()
.collect(Collectors.groupingBy(p -> p.getType() + "|" + p.getPrice()));

等价于,将分组依据单独抽取出一个方法,这样就可以加入比较复杂的逻辑了,最终返回的是一个字符串。

Map<String, List<Product>> map = productList.stream()
.collect(Collectors.groupingBy(p -> buildGroupKey(p)));

private static String buildGroupKey(Product p) {
return p.getType() + "|" + p.getPrice();
}

为什么两个字段之间要加一个分隔符呢,这是因为有些情况我们还会用到分组依据中的某一个字段,加入分隔符之后方便拆分字符串。当然了,也可以拿到这个分组下的任意一个元素获取。

嵌套分组

上面的根据多个字段分组是把多个字段当做同一级别并且的关系处理,还有一些时候呢,我们想要先按一个字段分组,再分组中再按另一个字段分组,这样就形成了一个嵌套关系,比如先按 type 分组,再按 price 分组,这就相当于是一个二维字典(两个层级)。

Map<Integer, Map<Double, List<Product>>> map = productList.stream()
.collect(Collectors.groupingBy(Product::getType, Collectors.groupingBy(Product::getPrice)));

通过返回值类型就可以看出来是怎么样的一个层级关系。

teeing()

这是 JDK 12 才出来的方法,所以要用这个方法,比如在 JDK12 以上才行。它的作用是对两个收集器(Collectors)的结果进行处理。上面的例子中,求出最高价格和最低价格的,并输出为一个字符串,将两个价格用 ~符号连接。

String result = productList.stream().collect(Collectors.teeing(
Collectors.minBy(Comparator.comparing(Product::getPrice)),
Collectors.maxBy(Comparator.comparing(Product::getPrice)),
(min, max) -> {
return min.get().getPrice() + "~" + max.get().getPrice();
}
));
System.out.println(result);

最终得到的结果是一个字符串,打印如下,测试数据没有做小数位限制。

4.347594572793579~89.43160979811124

最终的返回类型根据teeing() 方法的最后一个参数的返回结果而定。 min 和 max 这两个参数就是前两个收集器 Collectors.minByCollectors.maxBy的返回结果,因为返回类型是 Optional ,所以再取值的时候要加上 get

总结

Stream 提供了很丰富的 API ,最大的好处是让我们可以少写很多代码,熟练掌握之后,可以在一些对应的场景快速实现我们想要的逻辑。

有同学说,不行啊,又是 filter 、又是 collect、又是 Collectors ,根本记不住啊。没关系,记不住也正常,它本来就是一个工具,我们其实只要知道它可以实现什么功能,具体的用法可以随用随查吗。这不,我的这两篇文章就可以放进收藏夹里,什么时候用,什么时候打开查一下就好了。

下次碰到类似的场景,记得用 Stream 试一下吧。

风筝

作者

风筝

古时的风筝,一个平庸的程序员,主语言 Java,第二语言 Python,其实学 Python 的时间比 Java 还要早。喜欢写博客,写博客的过程能加深自己对一个知识点的理解,同时还可以分享给他人。喜欢做一些小东西,所以也会一些前端的东西,React、JavaScript、CSS 都会一些,做一些小工具还够用。